package org.aj.webapilog.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aj.base.annotation.IgnoreInputLog;
import org.aj.base.annotation.OutputLogRule;
import org.aj.base.web.response.ResponseResult;
import org.aj.webapilog.bean.LogTemplateVariable;
import org.aj.webapilog.config.WebApiLogProperties;
import org.aj.webapilog.user.LogUserInfo;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Parameter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * description  日志切面
 * @return:
 * @author: jcj
 * date : 2021/4/25 22:52
 */

public class PrintWebLog implements MethodInterceptor, Ordered {


    private static final Logger log = LoggerFactory.getLogger(PrintWebLog.class);

    /**
     * 用来从请求头获取请求属性里面取出请求时间的对于的key值名称
     */
    private static final String REQUEST_START_TIME="REQUEST_START_TIME";

    private static final String DEFAULT_LOG_TEMPLATE="{" +
            "\"requestUri\":%(requestUri)%,\"httpMethod\":%(httpMethod)%" +
            ",\"visitorIp\":%(visitorIp)%,\"visitorId\":%(visitorId)%" +
            ",\"visitorName\":%(visitorName)%,\"authorization\":%(authorization)%" +

            ",\"contentType\":%(contentType)%,\"requestParam\":%(requestParam)%" +


            ",\"responseResult\":%(responseResult)%,\"resultStatus\":%(resultStatus)%" +

            ",\"requestStartTime\":%(requestStartTime)%,\"requestEndTime\":%(requestEndTime)%,\"cost\":%(cost)%" +

            "}";

    private WebApiLogProperties webApiLogProperties;

    private int order= Ordered.LOWEST_PRECEDENCE;

    private LogUserInfo logUserInfo;

    public PrintWebLog(WebApiLogProperties webApiLogProperties, LogUserInfo logUserInfo){
        this.webApiLogProperties = webApiLogProperties;
        Integer aopOrder = webApiLogProperties.getAopOrder();
        if(ObjectUtils.isNotEmpty(aopOrder)){
            this.order = webApiLogProperties.getAopOrder();
        }
        this.logUserInfo = logUserInfo;

    }

    @Override
    public int getOrder() {
        return order;
    }


    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        ProceedingJoinPoint pjp = null;
        if (invocation instanceof ProxyMethodInvocation) {
            ProxyMethodInvocation pmi = (ProxyMethodInvocation) invocation;
            pjp = new MethodInvocationProceedingJoinPoint(pmi);
        }
        boolean isPrintInputLog;
        if(ObjectUtils.isNotEmpty(pjp)){
            isPrintInputLog = isPrintInputLog(pjp);
        }else{
            isPrintInputLog = isPrintInputLog(invocation);
        }

        //记录打印参数
        LogTemplateVariable logTemplateVariable = new LogTemplateVariable();

        long startTimestamp = System.currentTimeMillis();
        logTemplateVariable.setRequestStartTime(getDateTimeStr(startTimestamp));



        //得到Request对象
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        HttpServletRequest request = servletRequestAttributes.getRequest();

        logTemplateVariable.setHttpMethod(request.getMethod());

        logTemplateVariable.setVisitorIp(getHttpRequestIP(request));

        String reqPath = request.getRequestURI();

        String action = "";
        if(webApiLogProperties.getUrlBehindRequestParams()){
            action = handlerQueryParamWhenMethodNeed(request);
            if(StringUtils.isNotBlank(action)){
                action="?"+action;
                reqPath+=action;
            }
        }
        logTemplateVariable.setRequestUri(reqPath);

        //得到请求的媒体类型
        String contentType = request.getContentType();
        logTemplateVariable.setContentType(contentType);

        //看能否从请求域里面获取到请求参数若可以则不再进行解析请求参数
        Object attribute = request.getAttribute(webApiLogProperties.getRequestAttributeRequestParamKey());

        //记录入参
        String params = "";

        //记录请求头授权信息
        JSONObject paramHead = new JSONObject();



        if (isPrintInputLog) {

            //拿到参数值
            Object[] ob = invocation.getArguments();

            //若是有值才有解析参数名称的必要
            if(ObjectUtils.isNotEmpty(ob)){
                //得到方法签名 上的参数数组
                Parameter[] parameters = invocation.getMethod().getParameters();
                //得到参数名称
                String[] parametersName = getMethodParam(invocation, pjp);

                if(ObjectUtils.isEmpty(attribute)){
                    //构建json形式的 参数模型： key-参数名称 value-参数值
                    JSONObject jsonParam =  getJsonParam(parametersName,ob,parameters);

                    //转换成字符串
                    params = convertJsonParamToString(jsonParam,contentType,parameters);

                }

                //拿到请求头
                paramHead =  getJsonHead(parametersName,ob,parameters);
            }

        }

        //看能否从请求头里面拿到授权信息
        Map<String, Object> authMsg = getAuthMsg(request);
        paramHead.putAll(authMsg);

        //记录授权信息
        logTemplateVariable.setAuthorization(paramHead.toString());

        //记录用户信息
        setterUserInfo(logTemplateVariable,paramHead);

        if(StringUtils.isNotBlank(params)){
            //供下一链路获取
            request.setAttribute(webApiLogProperties.getRequestAttributeRequestParamKey(), params);
            logTemplateVariable.setRequestParam(params);
        }else if(ObjectUtils.isNotEmpty(attribute)){
            logTemplateVariable.setRequestParam(attribute.toString());
        }

        //往request域里面放入其实时间供下一链路获取
        request.setAttribute(REQUEST_START_TIME,startTimestamp);

        if(webApiLogProperties.isEnablePrintPreLog()){
            log.info(JSON.toJSONString(logTemplateVariable));
        }
        //执行 被切的方法
        Object retObject;
        try {
            retObject = invocation.proceed();
        } catch (Throwable throwable) {

            long endTimestamp = System.currentTimeMillis();

            //计算耗时
            String timeDiffStr = String.format("%.3f", (endTimestamp - startTimestamp) * 1.0 / 1000);
            JSONObject errorRes = new JSONObject();
            errorRes.put("error",throwable.getMessage());
            //记录请求结束时间
            logTemplateVariable.setRequestEndTime(getDateTimeStr(endTimestamp));
            logTemplateVariable.setResultStatus(false);
            logTemplateVariable.setResponseResult(errorRes.toJSONString());
            logTemplateVariable.setCost(Double.parseDouble(timeDiffStr));

            //打印日志
            printLog(logTemplateVariable);

            throw  throwable;
        }


        //得到返回值
        logTemplateVariable.setResponseResult(getRes(invocation, retObject));

        logTemplateVariable.setResultStatus(getResResult(retObject));

        //记录请求结束时间
        long endTimestamp = System.currentTimeMillis();

        //计算耗时
        String timeDiffStr = String.format("%.3f", (endTimestamp - startTimestamp) * 1.0 / 1000);

        logTemplateVariable.setRequestEndTime(getDateTimeStr(endTimestamp));
        logTemplateVariable.setCost(Double.parseDouble(timeDiffStr));
        //打印日志
        printLog(logTemplateVariable);


        return retObject;


    }

    private void setterUserInfo(LogTemplateVariable logTemplateVariable, JSONObject paramHead) {

       if(null != logUserInfo){
           logTemplateVariable.setVisitorId(logUserInfo.getUserId(paramHead));
           logTemplateVariable.setVisitorName(logUserInfo.getUserName(paramHead));
       }
    }

    /**
     * description 根据返回值判断 业务是否请求成功
     * 若返回值为空则返回 true ,若有只且可以转换为 {@link ResponseResult} 则根据状态码进行判断
     * 若不能转换为 目前则先返回true
     * @author aj
     * date 2023/1/27 14:59
     * @param retObject
     * @return boolean
     */
    private boolean getResResult(Object retObject) {

        if(ObjectUtils.isEmpty(retObject)){
            return false;
        }

        if(retObject instanceof ResponseResult){

            return (((ResponseResult) retObject).isSuccess());
        }

        return true;

    }

    private String convertJsonParamToString(JSONObject jsonParam, String contentType, Parameter[] parameters) {
        try {

            boolean b = StringUtils.isNotBlank(contentType) && contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)
                    && !jsonParam.isEmpty() && jsonParam.size() == 1 ;
            if(b){
                //若是携带了@ReuqestBody注解且参数只有一个值的情况下则不需要把key进行序列化，例如 public ResponseResult getData(@RequestBody entity)
                boolean anyMatch = Arrays.stream(parameters).anyMatch(parameter -> null != parameter.getAnnotation(RequestBody.class));
                if(anyMatch){
                    return  JSON.toJSONString(jsonParam.values().toArray()[0]);
                }
            }
            return JSON.toJSONString(jsonParam);

        } catch (Exception e) {
            log.warn("在把入参转换成json形式的字符串异常,故直接调用map的toString方法这样可以保证不出错",e);
            return jsonParam.getInnerMap().toString();
        }
    }

    private JSONObject getJsonHead(String[] parametersName, Object[] ob, Parameter[] parameters) throws ClassNotFoundException {

        //记录请求头
        JSONObject paramHead = new JSONObject();
        for (int i = 0; i < parametersName.length; i++) {

            if(isPrintParam(ob[i])){
                //这里需要判断下请求参数是不是携带了 RequestHead注解若是的话则放入到头部信息
                RequestHeader annotation = parameters[i].getAnnotation(RequestHeader.class);
                if(ObjectUtils.isEmpty(annotation)){
                  continue;
                }

                String value = annotation.value();
                if(StringUtils.isNotBlank(value)){
                    paramHead.put(value,ob[i]);
                }else if (StringUtils.isNotBlank(annotation.name())){
                    paramHead.put(annotation.name(),ob[i]);
                }else{
                    paramHead.put(parametersName[i],ob[i]);
                }
            }

        }
        return paramHead;
    }

    private JSONObject getJsonParam(String[] parametersName, Object[] ob,Parameter[] parameters) throws ClassNotFoundException {
        //记录入参
        JSONObject paramMap = new JSONObject();

        for (int i = 0; i < parametersName.length; i++) {

            if(isPrintParam(ob[i])){
                RequestHeader annotation = parameters[i].getAnnotation(RequestHeader.class);
                if(ObjectUtils.isNotEmpty(annotation)){
                    continue;
                }
                //判断是否进行了参数重命名
                RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
                String paramName;
                if(!ObjectUtils.isEmpty(requestParam) && (StringUtils.isNotBlank(requestParam.name())
                        ||StringUtils.isNotBlank(requestParam.value()))){
                    paramName = requestParam.name();
                    if(StringUtils.isBlank(paramName)){
                        paramName = requestParam.value();
                    }
                }else{
                    paramName = parametersName[i];
                }
                paramMap.put(paramName,ob[i]);
            }

        }

        return  paramMap;
    }

    private String[] getMethodParam(MethodInvocation invocation, ProceedingJoinPoint pjp) throws Exception {
        String[] parametersName;
        if(ObjectUtils.isNotEmpty(pjp)){
            Signature signature = pjp.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            //得到参数名称
            parametersName = methodSignature.getParameterNames();

        }else{
            //用java字节码增强工具去获取参数名称
            Class clazz = invocation.getThis().getClass();
            //获取调用的方法名
            String methodName = invocation.getMethod().getName();

            GetParameterName gp = new GetParameterName();
            parametersName = gp.getParameterNames(clazz, methodName);
        }
        return parametersName;
    }

    private void printLog(LogTemplateVariable logTemplateVariable) {
        String logTemplate = webApiLogProperties.getLogTemplate();
        if(StringUtils.isBlank(logTemplate)){
            logTemplate = DEFAULT_LOG_TEMPLATE;
        }

        //记录需要打印的参数
        Map<String,Object> printMap = new HashMap<>();
        printMap.put(LogTemplateVariable.VISITOR_IP_KEY,logTemplateVariable.getVisitorIp());
        printMap.put(LogTemplateVariable.VISITOR_ID_KEY,logTemplateVariable.getVisitorId());
        printMap.put(LogTemplateVariable.VISITOR_NAME_KEY,logTemplateVariable.getVisitorName());
        printMap.put(LogTemplateVariable.REQUEST_URI_KEY,logTemplateVariable.getRequestUri());
        printMap.put(LogTemplateVariable.HTTP_METHOD_KEY,logTemplateVariable.getHttpMethod());
        printMap.put(LogTemplateVariable.HTTP_CONTENT_TYPE_KEY,logTemplateVariable.getContentType());
        printMap.put(LogTemplateVariable.AUTHORIZATION_KEY,logTemplateVariable.getAuthorization());
        printMap.put(LogTemplateVariable.REQUEST_PARAM_KEY,logTemplateVariable.getRequestParam());
        printMap.put(LogTemplateVariable.RESPONSE_RESULT_KEY, logTemplateVariable.getResponseResult());
        printMap.put(LogTemplateVariable.RESULT_STATUS_KEY,logTemplateVariable.getResultStatus());
        printMap.put(LogTemplateVariable.REQUEST_START_TIME_KEY, logTemplateVariable.getRequestStartTime());
        printMap.put(LogTemplateVariable.REQUEST_END_TIME_KEY, logTemplateVariable.getRequestEndTime());

        printMap.put(LogTemplateVariable.COST_KEY, logTemplateVariable.getCost());
        //替换掉里面的变量
        logTemplate = replaceVariable(logTemplate, printMap);
        log.info(logTemplate);
    }

    private String getRes(MethodInvocation invocation, Object retObject) {
        String responseMsgLog = "";
        if(ObjectUtils.isNotEmpty(retObject) ){
            OutputLogRule annotation = invocation.getMethod().getAnnotation(OutputLogRule.class);
            if(ObjectUtils.isNotEmpty(annotation) && annotation.printData()){
                responseMsgLog = JSON.toJSONString(retObject);
            }else{
                if(retObject instanceof ResponseResult){
                    ResponseResult res = (ResponseResult) retObject;
                    responseMsgLog = res.toStringOfNoResult();
                }
            }
        }
        return responseMsgLog;
    }

    /**
     * description 从请求头里面获取请求参数里面获取授权信息  若没有则返回一个空map
     * @author aj
     * date 2022/8/7 16:13
     * @param request
     * @return java.util.Map<java.lang.String,java.lang.Object>
     */
    private Map<String,Object>  getAuthMsg(HttpServletRequest request) {
        Map<String,Object> authHead=new HashMap<>();
        String[] authorizationKey = webApiLogProperties.getAuthorizationKey();
        if(ObjectUtils.isEmpty(authorizationKey)){
            return authHead;
        }
        for (String s : authorizationKey) {

            String header = request.getHeader(s);
            if(StringUtils.isBlank(header)){
                header = request.getParameter(s);
            }
            if(StringUtils.isNotBlank(header)){
                authHead.put(s,header);
            }
        }
        return authHead;
    }

    private String handlerQueryParamWhenMethodNeed(HttpServletRequest request) {
        String queryString = request.getQueryString();
        if(StringUtils.isBlank(queryString) || !queryString.contains("="))
            return null;

        //只截取？后第一个key跟value

        int index = queryString.indexOf("&");
        if(index == -1){
            return queryString;
        }
        return queryString.substring(0,index);


/*        RequestMapping annotation
                = AnnotatedElementUtils.findMergedAnnotation(controllerMethod, RequestMapping.class);
        String[] queryParams = annotation.params();
        if(null != queryParams && queryParams.length > 0){
            for (String queryParam : queryParams) {
                if(queryString.contains(queryParam)){
                    return  "?"+queryParam;
                }
            }
        }*/
    }
    //判断是否需要打印参数
    private boolean isPrintParam(Object o) throws ClassNotFoundException {

        if(needPrintByType(o)){
            if(!isArray(o)){
                return true;
            }
            //若为数组在判断数组元素类型是否符合打印规则
            List list= new ArrayList(Arrays.asList(o));
            if(list.size() > 0){
                if(needPrintByType(list.get(0))){
                    return true;
                }
            }
        }
        return false;
    }

    private boolean needPrintByType(Object o) throws ClassNotFoundException {
        boolean b = null != o && !(o instanceof ServletRequest) && !(o instanceof ServletResponse)
                && !(o instanceof MultipartFile) && !(o instanceof BindingResult);
        if(!b){
            return false;
        }
        List<String> appendSkipPrintParamClass = webApiLogProperties.getAppendSkipPrintParamClass();
        if(ObjectUtils.isEmpty(appendSkipPrintParamClass)){
            return true;
        }
        for (String skipPrintParamClass : appendSkipPrintParamClass) {
            Class<?> aClass = Class.forName(skipPrintParamClass);
            if(aClass.isInstance(o)){
                return false;
            }
        }

        return true;
    }

    //利用根据时间戳得到日期形式的字符串
    private String getDateTimeStr(long timeStamp) {

        Instant instant = Instant.ofEpochMilli(timeStamp);
        LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return localDateTime.format(formatter);
    }

    //判断是否有必要打印日志
    private boolean isPrintInputLog(MethodInvocation joinPoint) {
        if (null != joinPoint.getThis().getClass().getAnnotation(IgnoreInputLog.class)) {
            return false;
        }

        return joinPoint.getMethod().getAnnotation(IgnoreInputLog.class) == null;
    }

    //判断是否有必要打印日志
    private boolean isPrintInputLog(ProceedingJoinPoint joinPoint) {

        if (null != joinPoint.getTarget().getClass().getAnnotation(IgnoreInputLog.class)) {
            return false;
        }

        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        return methodSignature.getMethod().getAnnotation(IgnoreInputLog.class) == null;
    }

    private static boolean isArray(Object obj) {
        return obj != null && obj.getClass().isArray();
    }



    /**
     * 获取请求IP
     *
     */
    public static String getHttpRequestIP(HttpServletRequest req) {

        String ip = req.getHeader("remote_request_address");
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getHeader("x-forwarded-for");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || ip.equalsIgnoreCase("unknown")) {
            ip = req.getRemoteAddr();
        }
        // 对于通过多个代理的情况，第一个IP为客户端真实IP,多个IP按照','分割
        // "***.***.***.***".length()
        if (ip != null && ip.length() > 15) {
            if (ip.indexOf(",") > 0) {
                ip = ip.substring(0, ip.indexOf(","));
            }
        }
        return ip;

    }

    /**
     * description 把模板里面的变量替换成对应的值
     * @author aj
     * date 2023/1/26 21:27
     * @param template
     * @param printMap
     * @return java.lang.String
     */
    private String replaceVariable(String template, Map<String, Object> printMap) {
        for (Map.Entry<String, Object> entry : printMap.entrySet()) {

            String key = entry.getKey();

            Object value = entry.getValue();
            if(value instanceof String){
                //给 字符串 加上双引号 这里为啥不采用拼接的方式带上 是考虑到 有可能 值是 json形式的字符串
                //调用此方法 可以对于json形式的字符串 带上转义字符 从而避免 后期使用者想转换成 json对象而出错。
                value = JSON.toJSONString(value);
            }
            if(value == null){
                template = template.replace("%(" + key + ")%", "null");
            }else{
                template = template.replace("%(" + key + ")%", value.toString());
            }

        }
        return template;
    }

    /**
     * 将模板中的参数转换成map的key-value形式
     * @param template
     * @return
     */
    private Map<String, Object> convertToMap(String template) {
        Map<String, Object> map = new HashMap<>();
        String[] arr = template.split("%\\(");
        for (String s : arr) {
            if (s.contains(")%")) {
                String key = s.substring(0,s.indexOf(")%"));
                //  String value = "null";
                map.put(key, "null");
            }
        }
        return map;
    }

}
